-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
iOS Side loading for captions and offline support #1109
Conversation
fix to respect User settings for captions.
@ashnfb Would you mind merging in the latest master? There's quite a bit that's changed in the main tree. |
# Conflicts: # ios/RCTVideo.m # package.json
@cobarx I've merged upstream into this fork. Should be good to go! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are some initial notes. I haven't gotten it to work quite yet.
Let me know on any of these if you need me to make the changes.
ios/RCTVideo.m
Outdated
} | ||
|
||
return [AVPlayerItem playerItemWithURL:url]; | ||
else if (!isNetwork && !isAsset) // this is a relative file hardcoded in the app? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be a simple else
clause
I believe files that are loaded via require
in JS are not treated as network or assets.
ios/RCTVideo.m
Outdated
@@ -809,7 +941,6 @@ - (void)usePlayerLayer | |||
// resize mode must be set before layer is added | |||
[self setResizeMode:_resizeMode]; | |||
[_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; | |||
_playerLayerObserverSet = YES; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is part of a player layer crash fix PR and needs to be reverted.
ios/RCTVideo.m
Outdated
@@ -18,7 +20,6 @@ @implementation RCTVideo | |||
BOOL _playerItemObserversSet; | |||
BOOL _playerBufferEmpty; | |||
AVPlayerLayer *_playerLayer; | |||
BOOL _playerLayerObserverSet; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is part of a player layer crash fix PR and needs to be reverted.
ios/RCTVideo.m
Outdated
@@ -848,10 +979,7 @@ - (void)setProgressUpdateInterval:(float)progressUpdateInterval | |||
- (void)removePlayerLayer | |||
{ | |||
[_playerLayer removeFromSuperlayer]; | |||
if (_playerLayerObserverSet) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is part of a player layer crash fix PR and needs to be reverted.
ios/RCTVideo.m
Outdated
} | ||
|
||
- (void)setSeek:(NSDictionary *)info |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of the changes in this function are part of a PR that supports seek tolerance, so the changes need to be reverted.
int sdk = android.os.Build.VERSION.SDK_INT; | ||
if (sdk>18 && groups.length>0) { | ||
CaptioningManager captioningManager = (CaptioningManager) themedReactContext.getSystemService(Context.CAPTIONING_SERVICE); | ||
if (captioningManager.isEnabled()) trackIndex=0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for figuring out this exists! I either forgot or didn't know this was available.
We should look for the track that matches the language the UI is set to. I believe we can use Locale.getDefault.getDisplayLanguage();
as stated here:
https://stackoverflow.com/questions/4212320/get-the-current-language-in-device
ios/RCTVideo.m
Outdated
@@ -666,17 +721,87 @@ - (void)setRepeat:(BOOL)repeat { | |||
_repeat = repeat; | |||
} | |||
|
|||
- (NSDictionary*) selectedTextTrack { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to get handled in some other way. It's not good practice to have getter methods that have side effects like modifying variables.
- (NSArray *)getTextTrackInfo | ||
{ | ||
|
||
// if sideloaded, textTracks will already be set | ||
if (_textTracks) return _textTracks; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On ExoPlayer, it's possible to use both sideloaded and streaming/embedded captions at the same time. Can we do the same here? As much as possible, I don't want to have to document exceptions or have functionality that varies between different platforms as it's confusing for people.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unclear how this works, so I've not attempted to do this.
ios/RCTVideo.m
Outdated
NSMutableArray *textTracks = [[NSMutableArray alloc] init]; | ||
AVMediaSelectionGroup *group = [_player.currentItem.asset | ||
mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; | ||
for (int i = 0; i < group.options.count; ++i) { | ||
AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; | ||
NSString *title = @""; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I was adding caption support, I ran into some videos that don't have any items in the metadata array, so grabbing index 0 will throw an exception. You can add a comment explaining that.
For the language, I think it makes more sense to return an empty string than a null value if that's not set.
ios/RCTVideo.m
Outdated
[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]]; | ||
|
||
AVURLAsset *asset; | ||
AVURLAsset *subAsset; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to avoid using subtitle as the name since text tracks can be subtitles or captions. Imo, text makes more sense. Not a big deal to leave this as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
Thank you for all your hard work on this! Ok, I started testing this and it's getting too late to figure out what I'm missing. A couple questions: Is it feasible to support more than one mutable text track? It's a pretty common situation where you have a menu and can select between English, French, Spanish, etc. Eventually, I'd like to see about making it possible to support sideloaded and streaming captions at the same time. I'm not sure it's super useful so it's not important atm. I can do the work to handle both at some point in the future. |
@cobarx thanks for the detailed feedback. Making the fixes and pushing them today. Cheers, Ash |
Hi @cobarx, I've pushed fixes in the PR for iOS and Android. Replies to some of your questions:
It seems like only VTT. I also tried .txtt and that didn't work.
From what I read, AVMutableCompositionTrack doesn't support AVMediaSelectionOption, which is what is used to switch between languages for Streaming video (AVMediaSelection also has built-in UI support, so that's a bummer we can't use it). We would have to build our own UI for switching tracks, and swap out the Mutable track for one language with another. |
Thanks for the quick turnaround on the changes. This is working for me. I tried with ttml and srt files and neither is working, so it does appear to be vtt only. I've been doing a little digging to see if we can support multiple tracks and figured out how to do this. Let's say you have 2 text tracks loaded via
And I get the second set of captions. If I flip the enabled values, I get the first set of captions. So it looks like the approach would be to load all the items in textTracks in playerItemFromSource. Then we need a way to figure out the track indexes in setSideloadedText and disable and enable the right ones. People have already requested multiple audio & video track support, so I don't want to assume that it's always 0: video, 1: audio, 2+: text. This code works to find the first text track index:
We can update the logic in setSideloadedText to disable all the text tracks but the one that matches the selectedTextTrack criteria. Do you want to implement this or should I? Edit: Make it more clear what setEnabled does. |
@@ -103,7 +105,7 @@ | |||
private boolean loadVideoStarted; | |||
private boolean isFullscreen; | |||
private boolean isInBackground; | |||
private boolean isPaused; | |||
private boolean isPaused = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a change that got wiped out during the merge. Please revert.
ios/RCTVideo.m
Outdated
|
||
[self addPlayerTimeObserver]; | ||
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are already in a delayed run loop, so I don't think we need a second one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, the extra dispatch can be removed.
@@ -762,7 +764,26 @@ public void setSelectedTextTrack(String type, Dynamic value) { | |||
trackIndex = value.asInt(); | |||
} else { // default. invalid type or "system" | |||
trackSelector.clearSelectionOverrides(index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this or the setSelectionOverride anymore? If we are SDK <= 18 or no groups, the trackIndex will be C.INDEX_UNSET and we'll clear the overrides further down.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cobarx we need the setSelectionOverride in cases where captions were previously enabled, and are now explicitly disabled (or the system settings are disabled). I've moved this up further in the code for clarity.
I have a few hours free this afternoon, so I'm going to start working on the text track selection for iOS piece. I wanted to make sure we don't duplicate effort. I'll post what I have at the end of the day. |
@cobarx thanks for the heads up. I'll comment on some of the iOS changes above, and I'll look into the Android changes then. |
clearSelectionOverride is still required for "default" and "disabled" case as it otherwise continues to show a selected track if it was previously selected
Ok I have the selection part working in 93ce4d6. Can you merge that into this PR. I am seeing one issue which is that if you specify |
@ashnfb Ok found a Stack Overflow that suggests it's not possible to use HLS streams in a mutable composition since tracks can be added & removed throughout playback: So I think this is ready to merge as soon as you pull in my commit and remove the redundant dispatch call. |
Pretty sure it all works, but would appreciate a review on my changes as well. |
I will go ahead and document everything after we're done. |
Ok, reviewing and merging now. |
@cobarx I've merged your changes, but unfortunately sideloading isn't working for me. It looks like _player is null when setSideloadedText is being called, which results in no text being enabled? |
What I was seeing is that setSelectedTrack was called multiple times. At first, _player is null but eventually everything is set, applyModifiers gets called, and then it enables the right stuff. |
Is it feasible to send me a copy of the code you use on the React side to create the player? Props get set in the native code in different orders depending on how you pass the props in. If not, I can probably trial and error it until I get the right combination. |
@cobarx invite sent to the repo. Thanks for offering to look into the problem. With the app, turn on Captions through device settings, and use the Search button to find film "Am Stram" (not all films have captions). In the React code, the props are sent by FilmDetail.js |
@ashnfb Got it, thanks. This is helpful. I found one issue. I have English set on my simulator and the captions are French. It was setting the wrong default index when there wasn't a caption matching the system language. Once I made the change, the captions showed up. I've committed my fix to this PR. I checked the code and setSelectedTextTrack is called as part of applyModifiers once the onLoad event fires, so I'm guessing the issue that I encountered is the same as yours and that _player being nil is not the problem. Please test against and let me know if there's still a problem. |
@cobarx thanks for investigating and solving the problem! works fine here too, Ready for merge 👍 |
Awesome, it's great to have full subtitle support! |
@cobarx Thanks for the hardwork, but i haven't understood where do we put the Url of the subtitle file, i mean, could you make an example or demo, of how does it work? or the correct steps to implement this feature |
Adds support for side-loading captions in iOS for both streaming and offline resources.